import _isFunction from "lodash/isFunction";
import { __read } from "tslib";
/**
 * @author Kuitos
 * @since 2019-10-21
 */

import { execScripts } from 'import-html-entry';
import { checkActivityFunctions } from 'single-spa';
import { frameworkConfiguration } from '../../apis';
import { getTargetValue, setProxyPropertyGetter } from '../common';
var styledComponentSymbol = 'Symbol(styled-component-qiankun)';
var attachProxySymbol = 'Symbol(attach-proxy-qiankun)';
var rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
var rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore;
var rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild;
var rawAppendChild = HTMLElement.prototype.appendChild;
var rawRemoveChild = HTMLElement.prototype.removeChild;
var SCRIPT_TAG_NAME = 'SCRIPT';
var LINK_TAG_NAME = 'LINK';
var STYLE_TAG_NAME = 'STYLE';
/**
 * Check if a style element is a styled-component liked.
 * A styled-components liked element is which not have textContext but keep the rules in its styleSheet.cssRules.
 * Such as the style element generated by styled-components and emotion.
 * @param element
 */

function isStyledComponentsLike(element) {
  var _a, _b;

  return !element.textContent && (((_a = element.sheet) === null || _a === void 0 ? void 0 : _a.cssRules.length) || ((_b = getCachedRules(element)) === null || _b === void 0 ? void 0 : _b.length));
}

function getCachedRules(element) {
  return element[styledComponentSymbol];
}

function setCachedRules(element, cssRules) {
  Object.defineProperty(element, styledComponentSymbol, {
    value: cssRules,
    configurable: true,
    enumerable: false
  });
}

function getNewAppendChild() {
  var args = [];

  for (var _i = 0; _i < arguments.length; _i++) {
    args[_i] = arguments[_i];
  }

  return function appendChild(newChild) {
    var element = newChild;

    if (element.tagName) {
      // eslint-disable-next-line prefer-const
      var _a = __read(args, 5),
          appName_1 = _a[0],
          appWrapperGetter = _a[1],
          proxy = _a[2],
          singular = _a[3],
          dynamicStyleSheetElements = _a[4];

      var storedContainerInfo = element[attachProxySymbol];

      if (storedContainerInfo) {
        // eslint-disable-next-line prefer-destructuring
        singular = storedContainerInfo.singular; // eslint-disable-next-line prefer-destructuring

        appWrapperGetter = storedContainerInfo.appWrapperGetter; // eslint-disable-next-line prefer-destructuring

        dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements; // eslint-disable-next-line prefer-destructuring

        proxy = storedContainerInfo.proxy;
      } // have storedContainerInfo means it invoked by a micro app


      var invokedByMicroApp = storedContainerInfo && !singular;

      switch (element.tagName) {
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME:
          {
            var stylesheetElement = newChild;

            if (invokedByMicroApp) {
              // eslint-disable-next-line no-shadow
              dynamicStyleSheetElements.push(stylesheetElement);
              return rawAppendChild.call(appWrapperGetter(), stylesheetElement);
            } // check if the currently specified application is active
            // While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering,
            // but the url change listener must to wait until the current call stack is flushed.
            // This scenario may cause we record the stylesheet from react routing page dynamic injection,
            // and remove them after the url change triggered and qiankun app is unmouting
            // see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230


            var activated = checkActivityFunctions(window.location).some(function (name) {
              return name === appName_1;
            }); // only hijack dynamic style injection when app activated

            if (activated) {
              dynamicStyleSheetElements.push(stylesheetElement);
              return rawAppendChild.call(appWrapperGetter(), stylesheetElement);
            }

            return rawHeadAppendChild.call(this, element);
          }

        case SCRIPT_TAG_NAME:
          {
            if (!invokedByMicroApp) {
              return rawAppendChild.call(this, element);
            }

            var _b = element,
                src = _b.src,
                text = _b.text;
            var fetch_1 = frameworkConfiguration.fetch;

            if (src) {
              execScripts(null, [src], proxy, {
                fetch: fetch_1,
                strictGlobal: !singular
              }).then(function () {
                // we need to invoke the onload event manually to notify the event listener that the script was completed
                // here are the two typical ways of dynamic script loading
                // 1. element.onload callback way, which webpack and loadjs used, see https://github.com/muicss/loadjs/blob/master/src/loadjs.js#L138
                // 2. addEventListener way, which toast-loader used, see https://github.com/pyrsmk/toast/blob/master/src/Toast.ts#L64
                var loadEvent = new CustomEvent('load');

                if (_isFunction(element.onload)) {
                  element.onload(loadEvent);
                } else {
                  element.dispatchEvent(loadEvent);
                }
              }, function () {
                var errorEvent = new CustomEvent('error');

                if (_isFunction(element.onerror)) {
                  element.onerror(errorEvent);
                } else {
                  element.dispatchEvent(errorEvent);
                }
              });
              var dynamicScriptCommentElement = document.createComment("dynamic script " + src + " replaced by qiankun");
              return rawAppendChild.call(appWrapperGetter(), dynamicScriptCommentElement);
            }

            execScripts(null, ["<script>" + text + "</script>"], proxy, {
              strictGlobal: !singular
            }).then(element.onload, element.onerror);
            var dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
            return rawAppendChild.call(appWrapperGetter(), dynamicInlineScriptCommentElement);
          }

        default:
          break;
      }
    }

    return rawHeadAppendChild.call(this, element);
  };
}

function getNewRemoveChild() {
  var args = [];

  for (var _i = 0; _i < arguments.length; _i++) {
    args[_i] = arguments[_i];
  }

  return function removeChild(child) {
    var _a = __read(args, 1),
        appWrapperGetter = _a[0];

    var storedContainerInfo = child[attachProxySymbol];

    if (storedContainerInfo) {
      // eslint-disable-next-line prefer-destructuring
      appWrapperGetter = storedContainerInfo.appWrapperGetter;
    }

    try {
      // container may had been removed while app unmounting
      var container = appWrapperGetter();

      if (container.contains(child)) {
        return rawRemoveChild.call(container, child);
      }
    } catch (e) {
      console.warn(e);
    }

    return rawHeadRemoveChild.call(this, child);
  };
}

function getNewInsertBefore() {
  var args = [];

  for (var _i = 0; _i < arguments.length; _i++) {
    args[_i] = arguments[_i];
  }

  return function insertBefore(newChild, refChild) {
    var element = newChild;

    if (element.tagName) {
      // eslint-disable-next-line prefer-const
      var _a = __read(args, 4),
          appName_2 = _a[0],
          appWrapperGetter = _a[1],
          singular = _a[2],
          dynamicStyleSheetElements = _a[3];

      var storedContainerInfo = element[attachProxySymbol];

      if (storedContainerInfo) {
        // eslint-disable-next-line prefer-destructuring
        singular = storedContainerInfo.singular; // eslint-disable-next-line prefer-destructuring

        appWrapperGetter = storedContainerInfo.appWrapperGetter; // eslint-disable-next-line prefer-destructuring

        dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements;
      }

      switch (element.tagName) {
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME:
          {
            var stylesheetElement = newChild; // have storedContainerInfo means it invoked by a micro app

            if (storedContainerInfo && !singular) {
              // eslint-disable-next-line no-shadow
              dynamicStyleSheetElements.push(stylesheetElement);
              return rawAppendChild.call(appWrapperGetter(), stylesheetElement);
            }

            var activated = checkActivityFunctions(window.location).some(function (name) {
              return name === appName_2;
            });

            if (activated) {
              dynamicStyleSheetElements.push(stylesheetElement);
              var wrapper = appWrapperGetter();
              var referenceNode = wrapper.contains(refChild) ? refChild : null;
              return rawHeadInsertBefore.call(wrapper, stylesheetElement, referenceNode);
            }

            return rawHeadInsertBefore.call(this, element, refChild);
          }

        default:
          break;
      }
    }

    return rawHeadInsertBefore.call(this, element, refChild);
  };
}

var patchCount = 0;
/**
 * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
 * Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, container),
 * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list.
 * @param appName
 * @param appWrapperGetter
 * @param proxy
 * @param mounting
 * @param singular
 */

export default function patch(appName, appWrapperGetter, proxy, mounting, singular) {
  if (mounting === void 0) {
    mounting = true;
  }

  if (singular === void 0) {
    singular = true;
  }

  var dynamicStyleSheetElements = [];

  if (!singular) {
    setProxyPropertyGetter(proxy, 'document', function () {
      return new Proxy(document, {
        get: function get(target, property) {
          if (property === 'createElement') {
            return function createElement(tagName, options) {
              var element = document.createElement(tagName, options);

              if ((tagName === null || tagName === void 0 ? void 0 : tagName.toLowerCase()) === 'style' || (tagName === null || tagName === void 0 ? void 0 : tagName.toLowerCase()) === 'script') {
                Object.defineProperty(element, attachProxySymbol, {
                  value: {
                    appName: appName,
                    proxy: proxy,
                    appWrapperGetter: appWrapperGetter,
                    dynamicStyleSheetElements: dynamicStyleSheetElements
                  },
                  enumerable: false
                });
              }

              return element;
            };
          }

          return getTargetValue(document, target[property]);
        },
        set: function set(target, p, value) {
          // eslint-disable-next-line no-param-reassign
          target[p] = value;
          return true;
        }
      });
    });
  } // Just overwrite it while it have not been overwrite


  if (HTMLHeadElement.prototype.appendChild === rawHeadAppendChild) {
    HTMLHeadElement.prototype.appendChild = getNewAppendChild(appName, appWrapperGetter, proxy, singular, dynamicStyleSheetElements);
  } // Just overwrite it while it have not been overwrite


  if (HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild) {
    HTMLHeadElement.prototype.removeChild = getNewRemoveChild(appWrapperGetter);
  } // `emotion` a css-in-js library insert a style tag use insertBefore, so we also rewrite it like appendChild
  // see https://github.com/umijs/qiankun/issues/420


  if (HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore) {
    HTMLHeadElement.prototype.insertBefore = getNewInsertBefore(appName, appWrapperGetter, singular, dynamicStyleSheetElements);
  }

  patchCount++;
  return function free() {
    patchCount--; // release the overwrite prototype after all the micro apps unmounted

    if (patchCount === 0) {
      HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
      HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore;
      HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
    }

    dynamicStyleSheetElements.forEach(function (stylesheetElement) {
      /*
         With a styled-components generated style element, we need to record its cssRules for restore next re-mounting time.
         We're doing this because the sheet of style element is going to be cleaned automatically by browser after the style element dom removed from document.
         see https://www.w3.org/TR/cssom-1/#associated-css-style-sheet
         */
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        if (stylesheetElement.sheet) {
          // record the original css rules of the style element for restore
          setCachedRules(stylesheetElement, stylesheetElement.sheet.cssRules);
        }
      } // As now the sub app content all wrapped with a special id container,
      // the dynamic style sheet would be removed automatically while unmoutting

    });
    return function rebuild() {
      dynamicStyleSheetElements.forEach(function (stylesheetElement) {
        // re-append the dynamic stylesheet to sub-app container
        // Using document.head.appendChild ensures that appendChild calls
        // can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase
        document.head.appendChild.call(appWrapperGetter(), stylesheetElement);
        /*
        get the stored css rules from styled-components generated element, and the re-insert rules for them.
        note that we must do this after style element had been added to document, which stylesheet would be associated to the document automatically.
        check the spec https://www.w3.org/TR/cssom-1/#associated-css-style-sheet
         */

        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          var cssRules = getCachedRules(stylesheetElement);

          if (cssRules) {
            // eslint-disable-next-line no-plusplus
            for (var i = 0; i < cssRules.length; i++) {
              var cssRule = cssRules[i];
              stylesheetElement.sheet.insertRule(cssRule.cssText);
            }
          }
        }
      }); // As the hijacker will be invoked every mounting phase, we could release the cache for gc after rebuilding

      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}